package cn.magictodo.weblog.common.aspect;

import cn.magictodo.weblog.common.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 切面类
 *
 * @author liu
 * @time 2023-11-14 00:27
 */

/**
 * @Aspect  声明该类为一个切面类；
 * @Component  Bean的组件化，表示该Bean可以被Spring容器管理并自动创建实例。
 */
@Slf4j
@Component
@Aspect
public class ApiOperationLogAspect {
    /** 以自定义 @ApiOperationLog 注解为切点，凡是添加 @ApiOperationLog 的方法，都会执行环绕中的代码
     * @Pointcut是一个注解，用于定义切面的执行时机，即定义哪些方法或代码块需要在程序执行时被拦截并执行特定的操作 */
    @Pointcut("@annotation(cn.magictodo.weblog.common.aspect.ApiOperationLog)")
    public void apiOperationLog() {}

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     * @Around是AspectJ中的一个注解，用于定义切面的环绕通知。
     * 环绕通知是在目标方法调用前后执行的，可以实现一些额外的操作，例如日志记录、性能统计等。
     * 使用@Around注解的pointcut可以匹配到所有的方法调用，包括类级别的方法和实例级别的方法。
     * 通过使用joinpoint，可以在目标方法调用前后执行自定义的代码。
     */
    @Around("apiOperationLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // 请求开始时间
            long startTime = System.currentTimeMillis();

           /* 将一个随机生成的UUID作为traceId放入MDC， traceId 表示跟踪 ID
           MDC（Mapped Diagnostic Context）是 SLF4J 和 log4j 等日志框架提供的一种方案，
           它允许开发者将一些特定的数据（如用户ID、请求ID等）存储到当前线程的上下文中，
           使得这些数据可以在日志消息中使用。这对于跟踪多线程或高并发应用中的单个请求非常有用。*/
            MDC.put("traceId", UUID.randomUUID().toString());

            // 获取被请求的类和方法
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();

            // 请求入参
            Object[] args = joinPoint.getArgs();
            // 入参转 JSON 字符串
            String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));

            // 功能描述信息
            String description = getApiOperationLogDescription(joinPoint);

            // 打印请求相关参数
            log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ",
                    description, argsJsonStr, className, methodName);

            // 执行切点方法
            Object result = joinPoint.proceed();

            // 执行耗时
            long executionTime = System.currentTimeMillis() - startTime;

            // 打印出参等相关信息
            log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ",
                    description, executionTime, JsonUtil.toJsonString(result));

            return result;
        } finally {
//            在请求结束时，为了避免污染其他请求，需要清除 MDC 中的值
            MDC.clear();
        }
    }

    /**
     * 获取注解的描述信息
     * @param joinPoint
     * @return
     */
    private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {
        // 1. 从 ProceedingJoinPoint 获取 MethodSignature （当前正在执行的方法的签名信息）
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 2. 使用 MethodSignature 获取当前被注解的 Method
        Method method = signature.getMethod();

        // 3. 从 Method 中提取 LogExecution 注解
        ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);

        // 4. 从 LogExecution 注解中获取 description 属性
        return apiOperationLog.description();
    }

    /**
     * 转 JSON 字符串
     * @return
     */
    private Function<Object, String> toJsonStr() {
        return arg -> JsonUtil.toJsonString(arg);
    }
}
